package com.starview.star;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.starview.platform.data.EvaluationException;
import com.starview.platform.data.IArray;
import com.starview.platform.data.IValue;
import com.starview.platform.data.type.ITypeAlias;
import com.starview.platform.data.type.ITypeDescription;
import com.starview.platform.data.type.Location;
import com.starview.platform.data.type.TypeContract;
import com.starview.platform.data.value.ResourceURI;
import com.starview.platform.resource.ResourceException;
import com.starview.platform.resource.Resources;
import com.starview.platform.resource.URIUtils;
import com.starview.platform.resource.catalog.Catalog;
import com.starview.platform.resource.catalog.CatalogException;
import com.starview.star.code.Manifest;
import com.starview.star.code.repository.CodeCatalog;
import com.starview.star.code.repository.CodeMemory;
import com.starview.star.code.repository.CodeRepository;
import com.starview.star.code.repository.CodeTree;
import com.starview.star.code.repository.RepositoryException;
import com.starview.star.code.repository.RepositoryManager;
import com.starview.star.compiler.CompilerUtils;
import com.starview.star.compiler.ErrorReport;
import com.starview.star.compiler.ast.Abstract;
import com.starview.star.compiler.ast.DisplayQuoted;
import com.starview.star.compiler.ast.IAbstract;
import com.starview.star.compiler.ast.StringLiteral;
import com.starview.star.compiler.cafe.CafeDisplay;
import com.starview.star.compiler.cafe.compile.CompileCafe;
import com.starview.star.compiler.canonical.PackageTerm;
import com.starview.star.compiler.canonical.TypeDefinition;
import com.starview.star.compiler.format.rules.FmtCompile;
import com.starview.star.compiler.format.rules.FmtProgram;
import com.starview.star.compiler.generate.CafeGenerator;
import com.starview.star.compiler.grammar.OpGrammar;
import com.starview.star.compiler.grammar.TermListener;
import com.starview.star.compiler.macrocompile.MacroCompiler;
import com.starview.star.compiler.operator.Operators;
import com.starview.star.compiler.sources.MetaRules;
import com.starview.star.compiler.sources.Pkg;
import com.starview.star.compiler.standard.StandardNames;
import com.starview.star.compiler.type.TypeChecker;
import com.starview.star.compiler.type.Visibility;
import com.starview.star.compiler.util.Instrument;
import com.starview.star.compiler.wff.WffEngine;
import com.starview.star.compiler.wff.WffProgram;

public class CompileDriver
{
  public static ResourceURI compilePackage(CodeRepository repository, ResourceURI uri, Catalog catalog, String hash,
      ErrorReport errors) throws ResourceException, CatalogException, RepositoryException
  {
    int errCount = errors.errorCount();
    boolean isPreamble = uri.getScheme().equals(Resources.STDSCHEME);
    boolean preludeOverride = !isPreamble || StarCompiler.SHOW_PRELUDE;

    Instrument timer = new Instrument("compile " + uri);

    String path = uri.getPath();
    String pkgName = URIUtils.getPackageName(uri);

    Location loc = Location.location(uri);

    Pkg pkg = new Pkg(uri, catalog, null, repository, errors);

    CodeCatalog bldCatalog = new CodeMemory(uri.getPath());
    Operators operators = Operators.operatorRoot().copy();
    WffProgram wffRules = new WffProgram();
    FmtProgram fmtRules = new FmtProgram();

    if (!isPreamble) {
      MetaRules meta = RepositoryManager.locateMeta(repository, StarCompiler.starRulesURI);
      assert meta != null;

      operators.importOperators(meta.getOperators(), errors, loc);
      wffRules.importRules(meta.getWffRules());
      fmtRules.importRules(meta.getFmtRules());
    }
    WffEngine validator = new WffEngine(errors, wffRules);
    OpGrammar parser = new OpGrammar(operators, errors);
    parser.addListener(new ImportOperators(repository, catalog, operators, errors));
    IAbstract term = null;
    try {
      try (Reader rdr = Resources.getReader(uri)) {
        term = parser.parse(uri, rdr, null);
        timer.record("parse");
      }
    } catch (ResourceException e) {
      errors.reportError("problem in accessing resource " + uri + ": " + e.getMessage(), loc);
    } catch (IOException e) {
      errors.reportError("IO problem in accessing resource " + uri + ": " + e.getMessage(), loc);
    }

    if (errors.noNewErrors(errCount)) {
      Operators pkgOps = new Operators();
      WffProgram pkgWff = new WffProgram();
      FmtProgram pkgFmt = new FmtProgram();

      List<IAbstract> macroStmts = new ArrayList<>();
      List<IAbstract> normalStmts = new ArrayList<>();

      findMetaRules(repository, term, errors, pkgOps, pkgWff, pkgFmt, catalog, macroStmts, normalStmts,
          Visibility.pUblic);

      if (!isPreamble) {
        IAbstract starImport = CompilerUtils.privateStmt(loc, CompilerUtils.importStmt(loc, new StringLiteral(loc,
            StarCompiler.starRulesURI.toString())));
        normalStmts.add(0, starImport);
        macroStmts.add(0, starImport);
      }
      if (!normalStmts.isEmpty())
        term = CompilerUtils.tupleUp(loc, StandardNames.TERM, normalStmts);
      else
        term = Abstract.name(loc, StandardNames.BRACES);

      final String macrolabel = pkgName + MacroCompiler.MACRO_QUERY;

      int mark = errors.errorCount();
      ResourceURI macroUri = MacroCompiler.macroUri(uri);
      IAbstract macro = MacroCompiler.compileMacroRules(loc, macroStmts, pkgName + MacroCompiler.MACRO_QUERY, catalog,
          repository, errors);

      if (errors.noNewErrors(mark) && macro != null) {
        if (StarCompiler.SHOWMACROCODE && preludeOverride)
          System.out.println(DisplayQuoted.display(macro));

        Pkg macroPk = new Pkg(macroUri, catalog, null, repository, errors);

        PackageTerm macroPkg = TypeChecker.typeOfPkg(macro, macroPk, errors);

        if (StarCompiler.SHOWCANON && preludeOverride)
          System.out.println(macroPkg);

        if (errors.noNewErrors(mark)) {
          IArray macroContent = CafeGenerator.generatePackage(macroPkg, errors);

          if (StarCompiler.SHOWCAFE && preludeOverride) {
            System.out.println("Cafe of macro-code " + pkgName + " at " + uri + " is ");
            System.out.println(CafeDisplay.display(macroContent));
          }

          CodeCatalog macroCatalog = new CodeMemory(path);
          CompileCafe.compileContent(macroUri, repository, URIUtils.rootPath(macroUri), macroPkg.getPkgName(), macro
              .getLoc(), macroContent, macroCatalog, errors);
          try {
            List<ITypeDescription> types = new ArrayList<>();
            List<ITypeAlias> aliases = new ArrayList<>();
            Map<String, TypeContract> contracts = new HashMap<>();

            // Extract the types that have been defined in this package
            for (TypeDefinition desc : macroPkg.getTypes())
              types.add(desc.getTypeDescription());

            for (ITypeAlias alias : macroPkg.getAliases())
              aliases.add(alias);
            for (TypeContract contract : macroPkg.getContracts())
              contracts.put(contract.getName(), contract);

            macroCatalog.addCodeEntry(StandardNames.MANIFEST, new Manifest(macroUri, macrolabel, types, aliases,
                contracts, macroPkg.getPkgType(), macroPkg.getPkgName()));

            repository.addRepositoryNode(macroUri, hash, macroCatalog);
            bldCatalog.addCodeEntry(path, macroCatalog);
            timer.record("compile macros");
          } catch (RepositoryException e) {
            errors.reportError(e.getMessage(), loc);
          }
        }
      }

      try {
        bldCatalog.addCodeEntry(StandardNames.METAENTRY,
            new MetaRules(uri, pkgName, pkgOps, macrolabel, pkgWff, pkgFmt));
      } catch (RepositoryException e) {
        errors.reportError(e.getMessage(), loc);
      }
      operators.importOperators(pkgOps, errors, loc);
      wffRules.importRules(pkgWff);
      fmtRules.importRules(pkgFmt);

      if (!isPreamble) {
        validator.validate(term, StandardNames.WFF_STATEMENT);
        timer.record("validation");
      }
      if (errors.isErrorFree()) {
        try {
          // This is a slight cheat, because the walker is the default replacer. But, the
          // top-level of a package is always 'interesting'
          term = (IAbstract) StarMain.invoke(repository, macroUri, macrolabel, new IValue[] { term }, timer, errors);
          timer.record("run macros");
          if (StarCompiler.TRACEMACRO)
            System.out.println("Macro replacement is " + term);
        } catch (EvaluationException e) {
          errors.reportError(e.getMessage(), e.getLoc());
        } catch (LanguageException e) {
          errors.mergeReport(e.getMessages());
        }

        if (StarCompiler.SHOWMACRO && preludeOverride)
          System.out.println(term);
      }
    }

    if (errors.isErrorFree()) {
      PackageTerm pkgTerm = TypeChecker.typeOfPkg(term, pkg, errors);
      if (pkgTerm.getUri() != null) {
        uri = pkgTerm.getUri();
      }

      timer.record("type check");

      if (StarCompiler.SHOWCANON && preludeOverride)
        System.out.println(pkgTerm.toString());
      if (errors.isErrorFree()) {

        IArray content = CafeGenerator.generatePackage(pkgTerm, errors);

        if (StarCompiler.SHOWCAFE && preludeOverride) {
          System.out.println("Cafe of " + pkgName + " at " + uri + " is ");
          System.out.println(CafeDisplay.display(content));
        }

        timer.record("cafe generate");

        if (errors.isErrorFree()) {
          String rootPath = URIUtils.rootPath(uri);
          CompileCafe.compileContent(uri, repository, rootPath, pkgTerm.getPkgName(), pkgTerm.getLoc(), content,
              bldCatalog, errors);

          timer.record("compile");

          List<ITypeDescription> types = new ArrayList<>();
          List<ITypeAlias> aliases = new ArrayList<>();
          Map<String, TypeContract> contracts = new HashMap<>();

          // Extract the types that have been defined in this package
          for (TypeDefinition desc : pkgTerm.getTypes())
            types.add(desc.getTypeDescription());

          for (ITypeAlias alias : pkgTerm.getAliases())
            aliases.add(alias);
          for (TypeContract contract : pkgTerm.getContracts())
            contracts.put(contract.getName(), contract);

          bldCatalog.addCodeEntry(StandardNames.MANIFEST, new Manifest(uri, pkgTerm.getName(), types, aliases,
              contracts, pkgTerm.getPkgType(), pkgTerm.getPkgName()));

          if (errors.isErrorFree()) {
            try {
              repository.addRepositoryNode(uri, hash, bldCatalog);
              timer.record("repository");
            } catch (RepositoryException e) {
              errors.reportError(e.getMessage(), pkgTerm.getLoc());
            }
          }
        } else
          repository.removeRepositoryNode(uri);
      }
    }

    if (StarCompiler.SHOWTIMING && preludeOverride)
      errors.reportInfo(timer.report());

    return uri;
  }

  private static class ImportOperators implements TermListener
  {
    private final CodeRepository repository;
    private final Operators operators;
    private final Catalog catalog;
    private final ErrorReport errors;

    public ImportOperators(CodeRepository repository, Catalog catalog, Operators operators, ErrorReport errors)
    {
      this.operators = operators;
      this.errors = errors;
      this.repository = repository;
      this.catalog = catalog;
    }

    @Override
    public void processTerm(IAbstract term)
    {
      if (CompilerUtils.isPrivate(term))
        processTerm(CompilerUtils.privateTerm(term));
      else if (CompilerUtils.isImport(term)) {
        IAbstract pkgRef = CompilerUtils.importPkg(term);
        Location loc = term.getLoc();

        try {
          MetaRules imported = RepositoryManager.locateMeta(repository, CompileDriver.uriOfPkgRef(pkgRef, catalog));

          if (imported != null) {
            operators.importOperators(imported.getOperators(), errors, loc);
          } else
            errors.reportWarning("cannot process import of " + pkgRef, term.getLoc());
        } catch (IllegalArgumentException e) {
          errors.reportError("cannot process import of " + pkgRef + "\nsince '" + pkgRef
              + "' is not a valid identifier for import.", loc);
        } catch (Exception e) {
          errors.reportWarning("cannot process import of " + pkgRef + "\nbecause " + e.getMessage(), term.getLoc());
        }
      }
    }
  }

  public static void validate(IAbstract term, ErrorReport errors, Location loc, String category,
      CodeRepository repository) throws ResourceException, CatalogException, RepositoryException
  {
    MetaRules meta = RepositoryManager.locateMeta(repository, StarCompiler.starRulesURI);
    assert meta != null;

    WffEngine validator = new WffEngine(errors, meta.getWffRules());

    switch (validator.validate(term, category)) {
    case validates:
      break;
    case notApply:
      errors.reportWarning("cannot find validation for " + term + " as " + category, loc);
      break;
    case notValidates:
      errors.reportError(term + " does not validate as " + category, loc);
      break;
    }
  }

  private static void findMetaRules(CodeRepository repository, IAbstract term, ErrorReport errors, Operators operators,
      WffProgram wffRules, FmtProgram fmtRules, Catalog catalog, List<IAbstract> macroStmts,
      List<IAbstract> normalStmts, Visibility visibility) throws CatalogException, ResourceException
  {
    for (IAbstract stmt : CompilerUtils.unWrap(term)) {
      if (CompilerUtils.isPackageStmt(stmt) && CompilerUtils.packageContents(stmt) != null) {
        List<IAbstract> pkgContent = new ArrayList<>();
        findMetaRules(repository, CompilerUtils.packageContents(stmt), errors, operators, wffRules, fmtRules, catalog,
            macroStmts, pkgContent, visibility);
        normalStmts.add(CompilerUtils.packageStmt(stmt.getLoc(), CompilerUtils.packageName(stmt), pkgContent));
      } else if (CompilerUtils.isPrivate(stmt))
        findMetaRules(repository, CompilerUtils.privateTerm(stmt), errors, operators, wffRules, fmtRules, catalog,
            macroStmts, normalStmts, Visibility.priVate);
      else if (CompilerUtils.isImport(stmt)) {
        if (visibility == Visibility.priVate) {
          IAbstract prStmt = CompilerUtils.privateStmt(stmt.getLoc(), stmt);
          normalStmts.add(prStmt);
          macroStmts.add(prStmt);
        } else {
          normalStmts.add(stmt);
          macroStmts.add(stmt);
        }
        IAbstract pkgName = CompilerUtils.importPkg(stmt);
        Location loc = stmt.getLoc();

        ResourceURI pkgUri = CompileDriver.uriOfPkgRef(pkgName, catalog);

        CodeCatalog imported = RepositoryManager.locatePackage(repository, pkgUri);
        if (imported != null) {
          try {
            CodeTree importMeta = imported.resolve(StandardNames.METAENTRY, MetaRules.EXTENSION);
            if (importMeta instanceof MetaRules) {
              MetaRules meta = (MetaRules) importMeta;
              operators.importOperators(meta.getOperators(), errors, loc);
              wffRules.importRules(meta.getWffRules());
              fmtRules.importRules(meta.getFmtRules());
            } else
              errors.reportError("cannot import " + pkgName, loc);
          } catch (RepositoryException e) {
            errors.reportError("cannot import " + pkgName + "\nbecause " + e.getMessage(), loc);
          }
        } else
          errors.reportError("cannot import " + pkgName, loc);
      } else if (Abstract.isUnary(stmt, StandardNames.META_HASH)) {
        IAbstract rl = Abstract.unaryArg(stmt);
        if (Abstract.isBinary(rl, StandardNames.WFF_RULE) || Abstract.isBinary(rl, StandardNames.WFF_DEFINES))
          wffRules.defineValidationRule(rl, errors);
        else if (Abstract.isBinary(rl, StandardNames.MACRORULE) || Abstract.isBinary(rl, StandardNames.IS)) {
          macroStmts.add(stmt);
        } else if (Abstract.isBinary(rl, StandardNames.FMT_RULE))
          fmtRules.defineFormattingRule(FmtCompile.compileRule(rl, fmtRules, errors));
        else
          operators.declareOperator(errors, rl);
      } else if (visibility == Visibility.priVate)
        normalStmts.add(CompilerUtils.privateStmt(stmt.getLoc(), stmt));
      else
        normalStmts.add(stmt);
    }
  }

  public static ResourceURI uriOfPkgRef(IAbstract pkg, Catalog catalog) throws ResourceException, CatalogException
  {
    if (Abstract.isIdentifier(pkg))
      return catalog.resolve(Abstract.getId(pkg));
    else if (pkg instanceof StringLiteral)
      return catalog.resolve(URIUtils.parseUri(Abstract.getString(pkg)));
    else if (Abstract.isParenTerm(pkg))
      return uriOfPkgRef(Abstract.deParen(pkg), catalog);
    else
      throw new ResourceException("invalid resource identifier: " + pkg);
  }
}
